using Dapper.ExpressionExtension.Resolvers;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace Dapper.ExpressionExtension.ExpressionMappers
{
    public static partial class ExpressionMapper
    {
        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="predicate"></param>
        /// <param name="transaction"></param>
        /// <param name="buffered"></param>
        /// <returns></returns>
        public static IEnumerable<TEntity> Select<TEntity>(this IDbConnection connection, Expression<Func<TEntity, bool>> predicate, IDbTransaction transaction = null, bool buffered = true)
        {
            var sql = BuildSelectSql(connection, predicate, out var parameters);
            LogQuery<TEntity>(sql);
            return connection.Query<TEntity>(sql, parameters, transaction, buffered);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="predicate"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        public static Task<IEnumerable<TEntity>> SelectAsync<TEntity>(this IDbConnection connection, Expression<Func<TEntity, bool>> predicate, IDbTransaction transaction = null)
        {
            var sql = BuildSelectSql(connection, predicate, out var parameters);
            LogQuery<TEntity>(sql);
            return connection.QueryAsync<TEntity>(sql, parameters, transaction);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="predicate"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        public static TEntity FirstOrDefault<TEntity>(this IDbConnection connection, Expression<Func<TEntity, bool>> predicate, IDbTransaction transaction = null)
            where TEntity : class
        {
            var sql = BuildSelectSql(connection, predicate, out var parameters);
            LogQuery<TEntity>(sql);
            return connection.QueryFirstOrDefault<TEntity>(sql, parameters, transaction);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="predicate"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        public static async Task<TEntity> FirstOrDefaultAsync<TEntity>(this IDbConnection connection, Expression<Func<TEntity, bool>> predicate, IDbTransaction transaction = null)
            where TEntity : class
        {
            var sql = BuildSelectSql(connection, predicate, out var parameters);
            LogQuery<TEntity>(sql);
            return await connection.QueryFirstOrDefaultAsync<TEntity>(sql, parameters, transaction);
        }

        private static string BuildSelectSql<TEntity>(IDbConnection connection, Expression<Func<TEntity, bool>> predicate, out DynamicParameters parameters)
        {
            var type = typeof(TEntity);

            var sql = BuildGetAllQuery(connection, type);

            sql += CreateSqlExpression<TEntity>(GetSqlBuilder(connection))
                .Where(predicate)
                .ToSql(out parameters);
            return sql;
        }

        internal static string BuildGetAllQuery(IDbConnection connection, Type type)
        {
            var sqlBuilder = GetSqlBuilder(connection);
            var cacheKey = new QueryCacheKey(QueryCacheType.GetAll, sqlBuilder, type);
            if (QueryCache.TryGetValue(cacheKey, out var sql))
            {
                return sql;
            }
            sql = "select * from " + ResolverHelper.Table(type, sqlBuilder);
            QueryCache.TryAdd(cacheKey, sql);

            return sql;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="predicate"></param>
        /// <param name="pageNumber"></param>
        /// <param name="pageSize"></param>
        /// <param name="transaction"></param>
        /// <param name="buffered"></param>
        /// <returns></returns>
        public static IEnumerable<TEntity> SelectPaged<TEntity>(this IDbConnection connection, Expression<Func<TEntity, bool>> predicate, int pageNumber, int pageSize, IDbTransaction transaction = null, bool buffered = true)
        {
            var sql = BuildSelectPagedQuery(connection, predicate, pageNumber, pageSize, out var parameters);
            LogQuery<TEntity>(sql);
            return connection.Query<TEntity>(sql, parameters, transaction, buffered);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="predicate"></param>
        /// <param name="pageNumber"></param>
        /// <param name="pageSize"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        public static Task<IEnumerable<TEntity>> SelectPagedAsync<TEntity>(this IDbConnection connection, Expression<Func<TEntity, bool>> predicate, int pageNumber, int pageSize, IDbTransaction transaction = null)
        {
            var sql = BuildSelectPagedQuery(connection, predicate, pageNumber, pageSize, out var parameters);
            LogQuery<TEntity>(sql);
            return connection.QueryAsync<TEntity>(sql, parameters, transaction);
        }

        private static string BuildSelectPagedQuery<TEntity>(IDbConnection connection, Expression<Func<TEntity, bool>> predicate, int pageNumber, int pageSize, out DynamicParameters parameters)
        {
            var sql = BuildSelectSql(connection, predicate, out parameters);

            var keyColumns = ResolverHelper.KeyProperties(typeof(TEntity)).Select(p => ResolverHelper.Column(p.Property, connection));
            var orderBy = "order by " + string.Join(", ", keyColumns);
            sql += GetSqlBuilder(connection).BuildPaging(orderBy, pageNumber, pageSize);
            return sql;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <typeparam name="TDto"></typeparam>
        /// <param name="connection"></param>
        /// <param name="selector"></param>
        /// <param name="predicate"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static string BuildSelectWhereSql<TEntity>(IDbConnection connection, Expression<Func<TEntity, TEntity>> selector, Expression<Func<TEntity, bool>> predicate, out DynamicParameters parameters)
        {
            var sqlExpression = CreateSqlExpression<TEntity>(GetSqlBuilder(connection)).Select(selector).Where(predicate);
            var sql = sqlExpression.ToSql(out parameters);
            return sql;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="connection"></param>
        /// <param name="selector"></param>
        /// <param name="predicate"></param>
        /// <param name="transaction"></param>
        /// <param name="buffered"></param>
        /// <returns></returns>
        public static IEnumerable<TEntity> SelectWhere<TEntity>(this IDbConnection connection, Expression<Func<TEntity, TEntity>> selector, Expression<Func<TEntity, bool>> predicate, IDbTransaction transaction = null, bool buffered = true)
        {
            var sql = BuildSelectWhereSql(connection, selector, predicate, out var parameters);
            LogQuery<TEntity>(sql);
            return connection.Query<TEntity>(sql, parameters, transaction, buffered);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="con"></param>
        /// <param name="sqlBuilder"></param>
        /// <param name="transaction"></param>
        /// <param name="buffered"></param>
        /// <returns></returns>
        public static IEnumerable<TEntity> SelectAdvance<TEntity>(this IDbConnection con, Action<SqlExpression<TEntity>> sqlBuilder, IDbTransaction transaction = null, bool buffered = true)
        {
            var sqlExpression = CreateSqlExpression<TEntity>(GetSqlBuilder(con));
            sqlBuilder(sqlExpression);
            var sql = sqlExpression.ToSql(out var parameters);
            return con.Query<TEntity>(sql, parameters, transaction, buffered);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="con"></param>
        /// <param name="sqlBuilder"></param>
        /// <param name="transaction"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<TEntity>> SelectAdvanceAsync<TEntity>(this IDbConnection con, Action<SqlExpression<TEntity>> sqlBuilder, IDbTransaction transaction = null)
        {
            var sqlExpression = CreateSqlExpression<TEntity>(GetSqlBuilder(con));
            sqlBuilder(sqlExpression);
            var sql = sqlExpression.ToSql(out var parameters);
            return await con.QueryAsync<TEntity>(sql, parameters, transaction);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="expression"></param>
        /// <param name="sqlBuilder"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static string SelectAdvance<TEntity>(SqlExpression<TEntity> expression, Action<SqlExpression<TEntity>> sqlBuilder, out DynamicParameters parameters)
        {
            sqlBuilder(expression);
            var sql = expression.ToSql(out parameters);
            return sql;
        }
    }
}
